/*******************************************************************************
 * \brief	进行串口0的回环输入输出
 * \details	可以通过Keil的UART #1调试串口直接输出串口信息和接收串口的输入，
 *			也可以通过VSPD虚拟串口软件与PC上的SSCOM32等串口软件进行绑定，通过
 *			串口软件收发信息。
 * \attention	Keil UART #1和SSCOM32窗口输入的行尾是\r，不是\r\n，
 *				但是输出又是以\r\n来处理的。Keil模拟器模式下，
 *				晶振不管设置为多少，UART0不管是否初始化，设置多少波特率，Keil
 *				UART #1调试串口的收发总是115200的波特率。
 * \author	将狼才鲸
 * \date	2022-10-30
 * \note	参考网址：
 *			[Keil实例仿真AT89C51串口UART收发数据(附程序)]
 *			(https://blog.csdn.net/tj_nonstoper/article/details/124271543)
 *			[keil MDK 中使用虚拟串口调试串口]
 *			(https://blog.csdn.net/NICHUN12345/article/details/124423615)
 *			[虚拟串口 VSPD 的使用]
 *			(https://blog.csdn.net/qq_17351161/article/details/89607458)
 ******************************************************************************/

/********************************** 头文件 ************************************/
#include <reg51.h>	/* 8051通用的寄存器定义 */

/********************************** 宏定义 ************************************/
#define CHAR_LOOPBACK		/* 收到一个字符就返回一个字符 */
//#define STRING_LOOPBACK	/* 收到一行后再返回一行，如果接收buffer满则先返回整个buffer数据 */
/* Keil使用自带的模拟器运行时，输入的换行是\r，但输出换行却是\r\n */
#define FIX_KEIL_EMULATOR_AND_SSCOM32_UART_LINE_FEED_ISSUE

#define BUF_MAX_SIZE 16		/* 设置短一点，即使没输入正确的换行符，也能及时有数据返回 */

/********************************* 全局变量 ***********************************/
static char rcv_buf[BUF_MAX_SIZE] = {0};	/* 完整的一行接收缓存 */
static int rcv_cnt = 0;	/* 接收的字节数 */
static char rcv_str_end_flag = 0; /* 0 is false, !0 is true */

static void uart0_init(void);
static void send_str(char *str);
static void send_char(char c);

//如果要使用printf，需要进行putchar输出重定义
//不使用printf，直接往串口写数据也能输出到UART #1
///**
// * \brief		printf重定向的函数，这里重定向到串口0
// * \param[in]	c	承接printf中传入的一个字符
// * \note		这里的函数注释格式是Doxygen，有兴趣的可以自行去了解
// * \return		正常时返回c，错误时返回错误码
// */
//char putchar(char c)
//{
//	ES = 0;				/* 关串口中断 */
//	SBUF = c;           
//	while (TI != 1);	/* 等待发送成功，产生发送中断 */
//	TI = 0;				/* 清除发送中断标志 */
//	ES = 1;				/* 开串口中断 */

//	return c;
//}

/********************************* 接口函数 ***********************************/
/**
 * \brief	入口函数
 */
int main()
{
	/* 1. 串口初始化 */
	uart0_init();

	/* 2. 打印提示信息 */
	send_str("====\r\n");
	send_str("| Check your terminal line end format.\r\n");
	send_str("| \r \\r \r");
	send_str("| \n \\n \n");
	send_str("| \n\r \\n\\r \n\r");
	send_str("| \r\n \\r\\n \r\n");
	send_str("====\r\n\r\n");

	send_str("====\r\n");
#ifdef STRING_LOOPBACK
	send_str("Now is string loopback mode\r\n");
	send_str("Please input string, and end with \\r or \\n.\r\n");
#else
	send_str("Now is char loopback mode\r\n");
	send_str("Please input char\r\n");
#endif

	/* 3. 打印从串口中断收到的信息 */
	while (1) {  	 
		if (rcv_str_end_flag) {
			/* 如果收到数据，则进行回环输出 */
			rcv_str_end_flag = 0;
#ifdef STRING_LOOPBACK
			send_str("Your Input: ");
			send_str(rcv_buf);  /* 把接收到的字符串发送回去 */
#else
			/* 防止有人不知道串口中断输入是没有回显的，原样输出会以为只是自己的输入 */
			send_char('>');
			send_char(rcv_buf[0]);
#	ifdef FIX_KEIL_EMULATOR_AND_SSCOM32_UART_LINE_FEED_ISSUE
			if (rcv_buf[0] == '\r')
				send_char('\n');
#	endif
			send_char(' ');
#endif

			rcv_buf[0] = '\0'; /* 清空字符串，直接写个字符串结尾 */
		}
	}
}	

/********************************* 私有函数 ***********************************/
/**
 * \brief		串口0初始化
 * \attention	Keil模拟器模式时可以不初始化，即使配置了参数，也始终以115200波特率输出
 * \note		不初始化的话能输出数据，但是进不了接收中断，接收不了信息
 */
static void uart0_init(void)
{
	/* 串口参数配置函数，这里配置为9600波特率，1位停止位，8位数据位，无校验 */
	TMOD = 0x20;  /* Timer1以定时模式工作在方式2：8位常数自动装入定时器/计数器 */
	SCON = 0x40;  /* SM0 = 0，SM1 = 1，方式1，10位UART "0 D0~D7 1"，波特率可变 */
	REN  = 1;     /* 允许串口接收数据位 */
	/** \warning 不管设置多少波特率，Keil模拟器在UART #1中的收发始终是115200 */
	TH1  = 0xFD;  /* 9600波特率：晶振的频率/(12 * (256 - 初值)) = 波特率 * 32 */
	TL1  = 0xFD;  /* 方式2的TH1，TL1是相等的，TL1自动重装TH1初值 */
	PCON = 0x00;  /* SMOD = 0波特率不加倍 */
	IE   = 0x90;  /* 允许总中断，允许串口中断，禁止其他中断 */
	PS   = 0;     /* 设置串行口中断优先级 */
	TR1  = 1;     /* 当GATE=0，TR1置“1” 启动定时器1 */
}
 
/**
 * \brief		发送一个字符
 * \param[in]	c	发送的字符
 */
static void send_char(char c)
{
   SBUF = c;		/* SBUF是指串行口同地址的两个缓冲寄存器，一个是发送寄存器，一个是接收寄存器，通过读和写来区分 */       
   while (!TI);		/* 等待一个字符发送完毕 */
   TI = 0;			/* TI软件清零，等待下一次发送后的置位 */
}

/**
 * \brief		发送一个字符串
 * \param[in]	str	要发送的字符串指针
 */
static void send_str(char *str)
{
	ES = 0; /* 发送前关闭串口中断，以防发送完每个字符都没必要地进入中断程序 */

	while (*str != '\0')		/* 持续发送直到字符串结尾 */
		send_char(*str++);	/* 先取值再自增 */

	ES = 1; /* 发送完打开串口中断 */
}
 
/**
 * \brief	串口0中断处理函数
 * \note	中断默认优先级：0外部中断0 > 1定时/计数器0 > 2外部中断1 > 3定时/计数器1 > 4串行中断
 */
void uart0_irq(void) interrupt 4 
{
	if (RI == 1) {
		/* RI置位表示一帧数据接收完毕，中断处理后RI必须用软件清0 */
		RI = 0; /* 中断标志位清零 */
		ES = 0; /* 关闭串口中断，防止下面程序执行时被打断，防止中断嵌套 */
#ifdef STRING_LOOPBACK
		if (rcv_cnt < BUF_MAX_SIZE - 2) {
			/* 接收缓冲区未溢出 */
			rcv_buf[rcv_cnt] = SBUF; /* 存放1个字节 */
			if (rcv_buf[rcv_cnt] == '\r' || rcv_buf[rcv_cnt] == '\n') {
				/* 如果接收到结束符（当前Keil UART #1和SSCOM都是\r） */
				rcv_str_end_flag = 1;	/* 接收了完整的字符串 */
#ifdef FIX_KEIL_EMULATOR_AND_SSCOM32_UART_LINE_FEED_ISSUE
				rcv_buf[rcv_cnt - 1] = '\n';
#endif
				rcv_buf[rcv_cnt] = '\0';	/* 在字符串末尾补字符串结束 */
				rcv_cnt = 0; /* 接收长度清0 */
			} else {
				rcv_cnt++;
			}
		} else {
			/* 接收缓冲区溢出 */
			rcv_str_end_flag = 1;
			rcv_buf[rcv_cnt] = '\0';
			rcv_cnt = 0;
		}
#else
		rcv_buf[0] = SBUF; /* 存放1个字节 */
		rcv_str_end_flag = 1;
#endif

		ES = 1;	/* 中断处理完重新打开串口中断 */
	}
}
